/**
 * 
 */
package spl.manager;

import java.net.HttpURLConnection;
import java.nio.ByteBuffer;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import spl.utils.GlobalLogger;

import com.google.common.base.Splitter;

import spl.manager.HTMLRenderer.UploadedTemplate;
import spl.manager.HTMLRenderer.GetPhotosTemplate;
import spl.manager.HTMLRenderer.GetAllPhotosTemplate;
import spl.manager.HTMLRenderer.GetJobsTemplate;
import spl.manager.HTMLRenderer.GetLinkRepresentationTemplate;

import spl.manager.JobsContainer.Job;

/**
 * @author avikam
 *
 */
public class HTTPRequestHandler {	
	private static interface HTTPBaseRequestHandler {
		HTTPResponse processRequest(SocketStream clientSocket, ArrayList<String> requestParams, Map<String, String> requestHeaders);
	}
	
	/**
	 * This request is used by clients to upload a new image (or resource). The manager should
	 * save the image (that is inside the body of the message), create a new resource, set the
	 * image as representation 0, and send a response. The response should be a 201 message
	 * with a link to the new resource page in the body.
	 * @author avikam
	 *
	 */
	private static class HTTPPutUploadRequestHandler implements HTTPBaseRequestHandler{
		@Override
		public HTTPResponse processRequest(SocketStream clientSocket, 
						ArrayList<String> requestParams, Map<String, String> requestHeaders) {
			

			try {
				// First of - read image
				int imSize = Integer.decode(requestHeaders.get("Content-Length"));				
				ByteBuffer im = HTTPRequestHandler.httpContentToMemorty(imSize, clientSocket);
				
				// Try to add image to stock
				int imageID = WorkManager.jobDisterbutor().addNewImage(im);
				// Serve a success page, on success
				if (imageID < 0) {
					return new HTTPResponse(HttpURLConnection.HTTP_INTERNAL_ERROR, "Internal I/O Error");
				}
				
				return new HTTPResponse(HttpURLConnection.HTTP_CREATED, "Created",
						new UploadedTemplate(imageID).render());
				
			} catch (NumberFormatException e) {
				return new HTTPResponse(HttpURLConnection.HTTP_LENGTH_REQUIRED);
			} catch (IOException e) {
				return new HTTPResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
			}
			
		}
	}
	
	/**
	 * handles /photos/[resource id].
	 * This request is also used by the client in order to submit a new job. The response should
	 * be a 202 message with a link to the output representation in the body
	 * @author avikam
	 *
	 */
	private static class HTTPPostPhotosRequestHandler implements HTTPBaseRequestHandler{
		private static int RES_ID = 0;
		@Override
		public HTTPResponse processRequest(SocketStream clientSocket, 
					ArrayList<String> requestParams, Map<String, String> requestHeaders) {
			
			int resId;
			
			if (null != requestParams.get(HTTPPostPhotosRequestHandler.RES_ID))
				resId = Integer.decode(requestParams.get(HTTPPostPhotosRequestHandler.RES_ID));	
			else
				return null;

			// First of - read job
			int xmlLen = Integer.decode(requestHeaders.get("Content-Length"));
			
			Job addedJob;
			try {
				addedJob = WorkManager.jobDisterbutor().addJob(resId, clientSocket.inputStream, xmlLen);
			} catch (UnsupportedEncodingException e) {
				return new HTTPResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
			}
			
			// If non such resource - indicate with 404
			if (null == addedJob)
				return null;
			
			return new HTTPResponse(HttpURLConnection.HTTP_ACCEPTED, "Created",
					new GetLinkRepresentationTemplate(addedJob.getResId(), addedJob.getDstRepId()).render());
		}
	}

	/**
	 * This request should result in a list of all the representations of a resource. Under each
	 * representation you should give a list of all the jobs that use that representation as an
	 * input. The result body is {@link HTMLRenderer#GetPhotosTemplate}
	 * @author avikam
	 *
	 */
	private static class HTTPGetPhotosRequestHandler implements HTTPBaseRequestHandler{
		private static int RESOURCE_ID = 0;
		@Override
		public HTTPResponse processRequest(SocketStream clientSocket,
				ArrayList<String> requestParams, Map<String, String> requestHeaders) {
			
			// If a photo is requested in the get method, search it
			if (null != requestParams.get(RESOURCE_ID))
				return this.getPhotos(Integer.decode(requestParams.get(RESOURCE_ID)));

			// Otherwise - return the index
			return this.getPhotos();
		}
		
		private HTTPResponse getPhotos(){
			return new HTTPResponse(HttpURLConnection.HTTP_OK, "OK", 
					new GetAllPhotosTemplate().render());
		}
		
		private HTTPResponse getPhotos(int resId){
			List<Representation> resourceRepresentations = null;
			
			resourceRepresentations	= WorkManager.jobDisterbutor().getPresentaionsFor(resId);
			
			// If resource does not exists, return null.
			if (null == resourceRepresentations) {
				return new HTTPResponse(HttpURLConnection.HTTP_NOT_FOUND, "NO Such Resource");
			}
			
			return new HTTPResponse(HttpURLConnection.HTTP_OK, "OK",
					new GetPhotosTemplate(resId, resourceRepresentations).render());			
		}
	}
	
	/**
	 * This request should result into a 200 OK response. The body should be the correct
	 * image. If there is no such representation the response should be 404. If there is such 
	 * representation, but it is not ready, the response should be 206.
	 * @author avikam
	 */
	private static class HTTPGetRepresentaionRequestHandler implements HTTPBaseRequestHandler{
		private static int RESOURCE_ID = 0;
		private static int REPRESENTATION_ID = 1;
		@Override
		public HTTPResponse processRequest(SocketStream clientSocket,
				ArrayList<String> requestParams, Map<String, String> requestHeaders) {
			// Parse parameters
			String resId = requestParams.get(RESOURCE_ID);
			String repId = requestParams.get(REPRESENTATION_ID);

			if (resId == null || repId == null)
				return null;
			
			Representation r = WorkManager.jobDisterbutor().getPresentaionFor(Integer.decode(resId),
																			Integer.decode(repId));
			
			if (null == r)
				return null;
			
			// Resource 0, has no job
			if (0 != r.getId()) {
				if (r.getCreatingJob().getStatus() != Job.FINISHED) {
					return new HTTPResponse(HttpURLConnection.HTTP_PARTIAL, "Be patient");
				}
			}
			
			return new HTTPResponse(HttpURLConnection.HTTP_OK,
					r.getContent(), r.getSize(), r.getMime());
		}
	}
	
	/**
	 * This request should result with a 200 OK response. In the body you should put the
	 * XHTML page that represent jobs.
	 * @author avikam
	 */
	private static class HTTPGetJobsRequestHandler implements HTTPBaseRequestHandler{
		private static int JOB_ID = 0;
		
		@Override
		public HTTPResponse processRequest(SocketStream clientSocket,
				ArrayList<String> requestParams,
				Map<String, String> requestHeaders) {
			
			String jobId = requestParams.get(JOB_ID);
			
			if (null == jobId)
				return this.getJobs();
				
				return this.getJobs(Integer.decode(jobId));
		}
		
		public HTTPResponse getJobs(int jobId) {
			Job j = null;
			try {
				j = WorkManager.jobDisterbutor().getAllJobs().get(jobId);
			} catch (IndexOutOfBoundsException e) {
				// If no such job - 404!
				return null;
			}
			if (null == j)
				return null;
			
			return new HTTPResponse(HttpURLConnection.HTTP_OK,
					j.getContent(), j.getSize(), "text/xml; charset=utf-8");
		}
				
		public HTTPResponse getJobs() { 
			List<Job> jobs = WorkManager.jobDisterbutor().getAllJobs();
			
			ArrayList<Job> submitted = new ArrayList<Job>();
			ArrayList<Job> nonSubmitted = new ArrayList<Job>();
			ArrayList<Job> finished = new ArrayList<Job>();
			
			synchronized(jobs) {
				for (Job j : jobs) {
					if (Job.SUBMITTED == j.getStatus())
						submitted.add(j);
					if (Job.PENDING == j.getStatus())
						nonSubmitted.add(j);
					if (Job.FINISHED == j.getStatus())
						finished.add(j);
				}
			}
			
			return new HTTPResponse(HttpURLConnection.HTTP_OK, "OK",
					new GetJobsTemplate(submitted, nonSubmitted, finished).render());
		}
	}
	
	/**
	 * This request is used by employees to ask for a new job. If there is an available job, the
	 * server should move the job to the submitted-jobs list and respond with a 200 message.
	 * The response should contain a custom header: resource: [resource id], as well as
	 * the job XML as described in Section 6.2.3. If there is no available job, the server should
	 * response with an empty 204 message.
	 * @author avikam
	 *
	 */
	private static class HTTPPostNewJobRequestHandler implements HTTPBaseRequestHandler{

		@Override
		public HTTPResponse processRequest(SocketStream clientSocket,
				ArrayList<String> requestParams,
				Map<String, String> requestHeaders) {
			
			Job j = WorkManager.jobDisterbutor().submitJob();
			if (null == j)
				return new HTTPResponse(HttpURLConnection.HTTP_NO_CONTENT);
			
			HTTPResponse hr = new HTTPResponse(HttpURLConnection.HTTP_OK,
					j.getContent(), j.getSize(), "text/xml; charset=utf-8");
			
			hr.putHeader("resource", Integer.toString(j.getResId()));
			
			return hr;
		}
		
	}
	
	/**
	 * 6.2.8 PUT /photos/[resource id]?rep=[output representation id]
	 * This request is to be used by employees only, in order to upload the output image of
	 * their work (in other words—representation). The manager should save the image (that 
	 * is inside the body of the message), connect the saved image to the appropriate
	 * representation ([output representation id]), move the job to the finished-jobs list,
	 * and send a response. The response 
	 * should be a 200 message with a link to the output representation page in the body.
	 *
	 * @author avikam
	 */
	private static class HTTPPutPhotoRequestHandler implements HTTPBaseRequestHandler{
		
		private static int RESOURCE_ID = 0;
		private static int REPRESENTATION_ID = 1;
		
		@Override
		public HTTPResponse processRequest(SocketStream clientSocket,
				ArrayList<String> requestParams,
				Map<String, String> requestHeaders) {
			
			String resId = requestParams.get(RESOURCE_ID);
			String repId = requestParams.get(REPRESENTATION_ID);

			if (resId == null || repId == null)
				return null;

			
			try {
				// First of - read image
				int imSize = Integer.decode(requestHeaders.get("Content-Length"));				
				ByteBuffer im = HTTPRequestHandler.httpContentToMemorty(imSize, clientSocket);
				
				// Try to add image to stock
				int imageID = WorkManager.jobDisterbutor().addImage(Integer.decode(resId),
																	Integer.decode(repId), im);
				// Serve a success page, on success
				if (imageID < 0) {
					return new HTTPResponse(HttpURLConnection.HTTP_INTERNAL_ERROR, "Internal I/O Error");
				}
				
				return new HTTPResponse(HttpURLConnection.HTTP_OK, "Ok",
						new UploadedTemplate(imageID).render());
				
			} catch (NumberFormatException e) {
				return new HTTPResponse(HttpURLConnection.HTTP_LENGTH_REQUIRED);
			} catch (IOException e) {
				return new HTTPResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
			}
		}
		
	}
	
	/**
	 * If a client calls this page, the server should shutdown gracefully.
	 * @author avikam
	 *
	 */
	private static class HTTPPostShutdownRequestHandler implements HTTPBaseRequestHandler{

		@Override
		public HTTPResponse processRequest(SocketStream clientSocket,
				ArrayList<String> requestParams,
				Map<String, String> requestHeaders) {
			WorkManager.stopSock();
			return new HTTPResponse(HttpURLConnection.HTTP_OK, "Shutting Down");
		}
	}
	
	private final static HashMap<Pattern, HTTPBaseRequestHandler> REGISTERED_HANDLERS = new HashMap<Pattern, HTTPBaseRequestHandler>() {
		private static final long serialVersionUID = 1L;

		{
			put(Pattern.compile("PUT /upload HTTP/(\\d+\\.\\d+)"), new HTTPPutUploadRequestHandler());
			put(Pattern.compile("PUT /photos/(\\d+)\\?rep=(\\d+) HTTP/(\\d+\\.\\d+)"), new HTTPPutPhotoRequestHandler());
			
			put(Pattern.compile("GET /jobs(?:/(\\d+))? HTTP/(\\d+\\.\\d+)"), new HTTPGetJobsRequestHandler());
			put(Pattern.compile("POST /jobs/get-new-job HTTP/(\\d+\\.\\d+)"), new HTTPPostNewJobRequestHandler());
			
			put(Pattern.compile("POST /photos/(\\d+) HTTP/(\\d+\\.\\d+)"), new HTTPPostPhotosRequestHandler());
			
			put(Pattern.compile("GET /photos(?:/(\\d+))? HTTP/(\\d+\\.\\d+)"), new HTTPGetPhotosRequestHandler());
			put(Pattern.compile("GET /photos/(\\d+)\\?rep=(\\d+) HTTP/(\\d+\\.\\d+)"), new HTTPGetRepresentaionRequestHandler());
			
			
			put(Pattern.compile("POST /shutdown HTTP/(\\d+\\.\\d+)"), new HTTPPostShutdownRequestHandler());
		}
	};
	
	/**
	 * process a client request.
	 * @param clientSocket 
	 * @return HTTP return value
	 * @throws IOException 
	 */
	public static HTTPResponse processRequest(SocketStream clientSocket) throws IOException {		
		// Read request and headers, terminated by /n/n
		String reqLine = clientSocket.readUnbufferredLine();
		Map<String,String> requestHeaders = parseHeaders(clientSocket);
		// Would would the handler's response result
		HTTPResponse res = null;
		
		// In each iteration, h is a handler that we check if the request should be
		// handled by it.
		for (Entry<Pattern, HTTPBaseRequestHandler> h : HTTPRequestHandler.REGISTERED_HANDLERS.entrySet()) {
			// Is this request matches the regex?
			Matcher matcher = h.getKey().matcher(reqLine);
			boolean matchFound = matcher.find();
			if (matchFound) {
				// Log operation
				GlobalLogger.getLogger().finest("Serving " + h.getValue());

				// Strip request parameters
				ArrayList<String> requestParams = new ArrayList<String>();
                // Starting from 1 to groupCount, just like docs/api/java/util/Scanner.html says...
                for(int i=1; i <= matcher.groupCount(); i++)
                       if (!requestParams.add(matcher.group(i)))
                               return new HTTPResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
            	
                // Let the correct handler, handle the request
                HTTPBaseRequestHandler handler = h.getValue();
                // Return the response code to caller
                res = handler.processRequest(clientSocket, requestParams, requestHeaders);
                // Don't search for more
                break;
			}
		}
		
		// In the case of no handlers responsible for this request - return 404
		if (null == res) {
			res = new HTTPResponse(HttpURLConnection.HTTP_NOT_FOUND);
		}

		// Render response to socket
		res.response(clientSocket);
		return res;
	}
	
	private static Map<String,String> parseHeaders(SocketStream clientSocket) throws IOException {
		String buf = "";
		String ln = clientSocket.readUnbufferredLine();
		while (!ln.isEmpty()) {
			buf += ln + "\n";
			ln = clientSocket.readUnbufferredLine();
		}
		
		// Check if buff is long enough
		if (buf.length() <= 1) {
			throw new IllegalArgumentException();
		}
		// Trim last new line
		buf = buf.substring(0, buf.length() - 1);
		
		return Splitter.on("\n").trimResults().withKeyValueSeparator(": ").split(buf);
	}
	
	/**
	 * Reads the content of the request into a buffer
	 * in memory of the process.
	 * @param contentLength is the number of bytes we expect to read (see HTTP RFC)
	 * @param clientSocket is the stream that we read from
	 * @return a buffer containing the content
	 * @throws IOException on socket reading execution
	 */
	public static ByteBuffer httpContentToMemorty(int contentLength, SocketStream clientSocket) throws IOException {
		ByteBuffer bf = ByteBuffer.allocate(contentLength);
		int maxTrasUnit = clientSocket.getMTU();
		byte[] buff = new byte[maxTrasUnit];
		// Read as much as needed and capable
		int readSize = Math.min(maxTrasUnit, contentLength);		 
		
		int lastRead = clientSocket.inputStream.read(buff, 0, readSize);
		int read = lastRead;
		
		// Keep on reading, until -1 (EOF) or reached the end
		while (lastRead > 0 && read < contentLength) {
			bf.put(buff, 0, lastRead);
			readSize = Math.min(maxTrasUnit, contentLength - read);
			lastRead = clientSocket.inputStream.read(buff, 0, readSize);
			read += lastRead;
			
		}
		
		// Append last read bytes
		if (lastRead > 0)
			bf.put(buff, 0, lastRead);
		
		return bf;
	} 
}
